在前一篇文章中,我們討論了如何動態生成 Card 元件 並構建了基礎的 Service Section,並利用 CSS Grid 實現了彈性且響應式的卡片佈局。今天,我們將繼續優化這個部分,探討如何在 React 單頁應用 中實現 平滑滾動與自動導航高亮 功能。透過這些技術,我們可以讓用戶點擊導航菜單時,頁面自動滾動到對應的區塊,並在滾動時自動高亮顯示當前區塊的導航項目。
單頁式應用和多頁式應用各自有不同的優勢和適用場景,下面是它們的簡單對比:
特點 | 單頁應用 (SPA) | 多頁應用 (MPA) |
---|---|---|
頁面加載 | 頁面只加載一次,內容通過 JavaScript 更新 | 每次頁面跳轉都會重新加載整個頁面 |
用戶體驗 | 提供更流暢的頁面切換體驗,減少頁面加載時間 | 可能會導致頁面間跳轉有延遲 |
SEO 支援 | 需要額外設置才能確保搜索引擎友好 | 默認情況下對搜索引擎友好 |
性能 | 適合內容變化頻繁的應用,但可能在首屏加載較慢 | 適合大型網站或多頁應用,首屏加載快,但跳轉慢 |
滾動與導航 | 可以利用平滑滾動和滾動監控技術模擬多頁切換效果 | 每次跳轉都加載新頁面,不需要特殊的滾動控制 |
在這篇文章中,我們將實現以下功能:
在第19天文章,我們已經為HeroSection
和 ServiceSection
設置一個唯一的 id
,這樣才能讓頁面根據導航的點擊進行滾動。接下來,我們將展示如何使用 React 實現單頁應用的平滑滾動導航。整個過程將分為三個步驟來完成,並在每個步驟中解釋具體的技術細節。
在單頁應用中,我們經常會遇到導航欄隨頁面滾動而消失的情況。為了提升用戶體驗,我們希望導航欄在滾動過程中保持可見,這樣用戶無需回到頂部即可快速導航。
解決方案是使用 position: sticky
,這是一個常用的 CSS 屬性,適合在滾動到某一位置時將元素固定在視口內。
//src/components/Layout.module.scss
header {
position: sticky;
height: 60px;
top: 0;
width: 100%;
z-index: 100;
background-color: var(--background-primary);
}
說明:
position: sticky
:當滾動到設置的 top
值時,導航欄將固定在視口頂部。這樣可以讓導航欄在滾動過程中保持可見,而未滾動到該位置前,它仍然像普通元素一樣滾動。z-index
:設置為 100,確保導航欄在滾動過程中不會被其他元素遮蓋。這樣,導航欄就不會隨著頁面的滾動而消失,提升了用戶的操作便利性。
接下來,我們將實現一個名為 handleScroll
的滾動處理函數,該函數可以在點擊導航欄後,將頁面平滑滾動至對應區塊。通過結合 scrollIntoView
和 window.scrollTo
兩種方法,我們能夠精確控制滾動行為,並避免目標區塊被固定導航欄遮擋。
// 定義滾動函數,實現平滑滾動並解決導航欄遮擋問題
const handleScroll = (id) => {
const element = document.getElementById(id);
if (id === 'home') {
window.scrollTo({
top: 0, // 滾動到頁面最上方
behavior: 'smooth' // 平滑滾動效果
});
} else {
const element = document.getElementById(id);
if (element) {
// 手動計算滾動位置,避免被導航欄遮擋
const elementPosition = element.getBoundingClientRect().top + window.scrollY - 80;
window.scrollTo({
top: elementPosition,
behavior: 'smooth',
});
}
}
};
說明:
window.scrollTo()
:對於點擊 "Home" 的情況,頁面將平滑滾動至頂部。getBoundingClientRect().top + window.scrollY
:這段程式碼計算出目標元素在整個頁面中的絕對位置,並將滾動位置調整,以避免被固定的導航欄遮擋。behavior: 'smooth'
:此選項啟用平滑滾動,讓滾動過程更加自然,增強用戶體驗。透過這些設定,點擊導航項目後頁面將順暢地滾動至目標區塊,同時確保目標區塊不會被固定的導航欄隱藏。
useScrollSpy
自動切換導航項目接著,透過一個自定義的 Hook useScrollSpy
來監控頁面滾動並自動更新導航的狀態。這主要依賴於 IntersectionObserver
API,來監控頁面上的各個區塊,當某個區塊進入視口時,導航欄對應的項目會自動高亮顯示。
// src/util/useScrollSpy.js
import { useEffect } from 'react';
const useScrollSpy = (setActiveLink) => {
useEffect(() => {
const sectionIds = ['home', 'services', 'projects', 'contact'];
const sections = sectionIds.map(id => document.getElementById(id));
const observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const index = sectionIds.indexOf(entry.target.id);
setActiveLink(index); // 根據進入視口的區塊更新菜單項目高亮
}
});
},
{ threshold: 0.6 } // 60% 區塊可見時觸發
);
sections.forEach(section => {
if (section) observer.observe(section);
});
return () => {
sections.forEach(section => {
if (section) observer.unobserve(section);
});
};
}, [setActiveLink]);
};
export default useScrollSpy;
說明:
IntersectionObserver
:通過觀察頁面中的區塊(如 "home"、"services" 等),當某個區塊進入視口時,自動高亮對應的導航項目。threshold: 0.6
表示當區塊 60% 進入視口時觸發觀察器。接下來,將滾動處理函數和自動高亮useScrollSpy
應用到導航欄中。當用戶點擊某個項目時,觸發滾動,並在滾動到對應區塊時自動高亮對應的導航項。
//src/components/navBar/navBar.jsx
import React, { useState } from 'react'
import ThemeButton from '@/components/navBar/ThemeButton';
import * as styles from '@/components/navBar/Navbar.module.scss';
import LangButton from '@/components/navBar/LangButton';
import useScrollSpy from '@/utils/useScrollSpy';
const Navbar = () => {
const [isMenuOpen, setIsMenuOpen] = useState(false); // 控制菜單開關
const [activeLink, setActiveLink] = useState(0); // 控制當前選中的菜單項目
// 定義選單項目
const menuItems = [
{ name: 'Home', link: '#home' },
{ name: 'Services', link: '#services' },
{ name: 'Projects', link: '#projects' },
{ name: 'Contact', link: '#contact' }
];
// 切換漢堡菜單的開關狀態
const toggleMenu = () => {
setIsMenuOpen(!isMenuOpen);
};
// 使用自定義 hook 來監控滾動
useScrollSpy(setActiveLink);
// 定義滾動並設置激活狀態的函數
const handleMenuClick = (e, index, link) => {
e.preventDefault(); // 防止默認的錨點行為
setActiveLink(index); // 設定激活的菜單項目
handleScroll(link); // 滾動到對應區塊
};
// 定義滾動函數,實現平滑滾動
const handleScroll = (id) => {
if (id === 'home') {
window.scrollTo({
top: 0, // 滾動到頁面最上方
behavior: 'smooth' // 平滑滾動效果
});
} else {
const element = document.getElementById(id);
if (element) {
element.scrollIntoView({ behavior: 'smooth' });
}
}
};
return (
<div className={styles.navbar} >
{/* 導航欄品牌區域 */}
<div className={styles.navbar_brand}>
<a href="/" className="navbar-brand">
<div className={styles.navbar_logo} />
</a>
</div>
{/* 側邊菜單區域 */}
<div className={`${styles.sideMenu} ${isMenuOpen && styles.menuOpen}`}>
<div className={styles.navbarLinks}>
{menuItems.map((item, index) => (
<li key={index} >
<a
href={item.link}
className={activeLink === index ? styles.active : ''}
onClick={(e) => handleMenuClick(e, index, item.link.substring(1))} // 呼叫獨立函數
>
{item.name}
</a>
</li>
))}
</div>
{/* 主題切換按鈕 */}
<ThemeButton className={styles.themeButton} />
<LangButton />
</div>
{/* 漢堡菜單按鈕 */}
<button className={styles.hamburgerMenu} onClick={toggleMenu}>
{isMenuOpen ? '✕' : '☰'}
</button>
</div >
)
}
export default Navbar
在這篇文章中,我們展示了如何實現 React 單頁應用的平滑滾動與導航高亮。透過 scrollIntoView 和 IntersectionObserver,我們不僅提升了頁面的流暢性,還讓用戶在滾動時能夠輕鬆跟蹤當前所在的位置。
完整Service Section程式碼已上傳至 GitHub,歡迎查看並進行進一步的學習與優化。
👉 前往 GitHub 的 v0.16.0-service-section-using-card 查看完整程式碼。
✨ 流光館Luma<∕> ✨ 期待與你繼續探索更多技術知識!